package com.ztesoft.book.admin.controller.sys;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.RSA;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.ztesoft.book.admin.annotation.SysLog;
import com.ztesoft.book.admin.config.captcha.CaptchaUtil;
import com.ztesoft.book.common.constant.Constant;
import com.ztesoft.book.common.enums.IntEnum;
import com.ztesoft.book.common.utils.Result;
import com.ztesoft.book.common.utils.base.SnowFlakeGenerator;
import com.ztesoft.book.common.utils.encrypt.AesEncryptSm4Util;
import com.ztesoft.book.common.utils.encrypt.RSAUtil;
import com.ztesoft.book.common.utils.exception.ExUtil;
import com.ztesoft.book.common.utils.exception.ServiceException;
import com.ztesoft.book.common.utils.jwt.JwtUtil;
import com.ztesoft.book.common.utils.redis.RedisUtils;
import com.ztesoft.book.core.base.base.enums.YesNoEnum;
import com.ztesoft.book.core.sys.sysdict.dto.SysDictDTO;
import com.ztesoft.book.core.sys.sysdict.enums.SysDictStateEnum;
import com.ztesoft.book.core.sys.sysdict.service.SysDictService;
import com.ztesoft.book.core.sys.sysloginhis.SysLoginHisVO;
import com.ztesoft.book.core.sys.sysloginhis.service.SysLoginHisService;
import com.ztesoft.book.core.sys.sysuser.dto.SysUserDTO;
import com.ztesoft.book.core.sys.sysuser.qo.SysLoginEncryptedQO;
import com.ztesoft.book.core.sys.sysuser.qo.SysLoginQO;
import com.ztesoft.book.core.sys.sysuser.qo.SysUserQO;
import com.ztesoft.book.core.sys.sysuser.service.SysUserService;
import com.ztesoft.book.core.sys.sysuserrole.service.SysUserRoleService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * @Description 登录相关
 * @Author YangFan
 * @Date 2021-06-22 17:06
 */
@Slf4j
@RestController
public class SysLoginController {

    private static String redisTokenExpireTime;

    @Value("${book.time.redis-token-expire}")
    public void setRedisTokenExpireTime(String redisTokenExpireTime1) {
        setRedisTokenExpireTimeValue(redisTokenExpireTime1);
    }

    public static void setRedisTokenExpireTimeValue(String redisTokenExpireTime1) {
        SysLoginController.redisTokenExpireTime = redisTokenExpireTime1;
        redisTokenExpireTime = redisTokenExpireTime1;
    }

    @Resource
    private SysUserService sysUserService;


    @Resource
    private SysDictService sysDictService;

    @Resource
    private SysLoginHisService sysLoginHisService;

    @Resource
    private SysUserRoleService sysUserRoleService;

    /**
     * 需不需要首次登录强制修改密码
     */
    public static final String FORCED_MODIFICATION = "1";
    
    /**
     * 判断是否是三十天强制更换密码
     */
    public static final String FORCED_MODIFICATION_THIRTY = "3";
    
    /**
     * unknown字符串
     */
    public static final String UN_KNOWN = "unknown";

    /**
     * 方法功能描述:
     * < 获取验证码 >
     *
     * @param response 【参数 1 注释说明】
     * @param uuid     【参数 2 注释说明】
     * @return void                            【返回值 注释说明】
     * @author YangFan
     * @date 2021-06-22 16:17
     */
    @GetMapping("captcha.jpg")
    public void captcha(HttpServletResponse response, String uuid) throws IOException {
        response.setHeader("Cache-Control", "no-store, no-cache");
        response.setContentType("image/jpeg");

        //获取图片验证码
        BufferedImage image = CaptchaUtil.getCaptcha(uuid);

        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(image, "jpg", out);
        IOUtils.closeQuietly(out);
    }


    /**
     * 方法功能描述:
     * < 系统登录 >
     *
     * @param httpServletResponse 【参数 1 注释说明】
     * @param form                【参数 2 注释说明】
     * @return Map map
     * @author YangFan
     * @date 2021-06-22 16:17
     */
    @SysLog("用户登录")
    @PostMapping("/login")
    public Map<String, Object> login(HttpServletResponse httpServletResponse, @RequestBody SysLoginQO form) {
        boolean captcha = CaptchaUtil.validate(form.getUuid(), form.getCaptcha());
        if (!captcha) {
            return Result.error("验证码不正确");
        }


        // 对传入的用户名进行ext加密，再与数据库比对
        SysUserQO qo = new SysUserQO();
        qo.setUsername(AesEncryptSm4Util.encrypt(form.getUsername()));

        SysUserDTO userDtoTemp = sysUserService.findOne(qo);
        if (userDtoTemp == null) {
            return Result.error("用户名或密码错误");
        }
        //拥有的权限列表
        List<String> roleIdList = sysUserRoleService.getRoleIdListByUserId(userDtoTemp.getId());
        userDtoTemp.setRoleIdList(roleIdList);
        ExUtil.checkNotNull(userDtoTemp, "该帐号不存在");
        ExUtil.checkArgument(YesNoEnum.NO.getValue()
                .equals(userDtoTemp.getStatus()), "该帐号已停用，请联系管理员");
        ExUtil.checkArgument(Objects.isNull(userDtoTemp.getUserLockTime()) || LocalDateTime.now()
                .isAfter(userDtoTemp.getUserLockTime()), "该帐号已锁定，请联系管理员解锁");


        UpdateWrapper<SysUserDTO> wrapper = new UpdateWrapper<>();

        // 密码 + 用户名进行国密SM3加密，再与数据库比对
        String key = SmUtil.sm3(userDtoTemp.getSalt() + form.getPassword());
        if (key.equals(userDtoTemp.getPassword())) {
            // 清除可能存在的Shiro权限信息缓存
            if (RedisUtils.exists(Constant.PREFIX_SHIRO_CACHE + form.getUsername())) {
                RedisUtils.delete(Constant.PREFIX_SHIRO_CACHE + form.getUsername());
            }
            // 清除锁定次数和时间
            wrapper.lambda()
                    .eq(SysUserDTO::getId, userDtoTemp.getId())
                    .set(SysUserDTO::getWrongPwdTimes, IntEnum.ZERO.getValue())
                    .set(false, SysUserDTO::getUserLockTime, null);
            sysUserService.update(wrapper);
            // 设置RedisToken，时间戳为当前时间戳，并且写入相关登录用户信息，直接设置即可(不用先删后设，会覆盖已有的RedisToken)
            String currentTimeMillis = String.valueOf(System.currentTimeMillis());
            Map<String, Object> redisToken = new HashMap<>(16);
            redisToken.put("currentTimeMillis", currentTimeMillis);
            redisToken.put("user", userDtoTemp);
            RedisUtils.set(Constant.PREFIX_SHIRO_REFRESH_TOKEN + form.getUsername(), redisToken, Integer.parseInt(redisTokenExpireTime));
            // 从Header中Authorization返回AccessToken，时间戳为当前时间戳
            String token = JwtUtil.signUserExt(userDtoTemp.getId() + "", userDtoTemp.getNickName(),
                    userDtoTemp.getLogo(), form.getUsername(), currentTimeMillis);
            httpServletResponse.setHeader("token", token);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "token");
            Map loginObj = new HashMap(16);
            loginObj.put("token", token);
            loginObj.put("nickName", userDtoTemp.getNickName());
            loginObj.put("logo", userDtoTemp.getLogo());
            loginObj.put("userid", userDtoTemp.getId());
            loginObj.put("roleId", userDtoTemp.getRoleId());
            // 是否修改过密码。0：否，1：是，如果是的话，前端不弹出修改页面。
            SysDictDTO sysDictDTO = sysDictService.findById(FORCED_MODIFICATION);
            //0：正常 1：作废.
            if (YesNoEnum.NO.getValue()
                    .equals(sysDictDTO.getState())) {
                //需要在未修改的情况下强制修改密码
                loginObj.put("modifyPwd", userDtoTemp.getModifyPwd());
            }
            else {
                loginObj.put("modifyPwd", YesNoEnum.YES.getValue());
            }

            // 判断是否是三十天强制更换密码
            SysDictDTO sysDictUpdatePassWord = sysDictService.findById(FORCED_MODIFICATION_THIRTY);
            if (SysDictStateEnum.NORMAL.getValue().equals(sysDictUpdatePassWord.getState()) && ObjectUtil.isNotEmpty(sysDictUpdatePassWord.getRemark())) {
                LocalDateTime checkTime = userDtoTemp.getCreateTime();
                // 未修改过密码没有改密时间的按照创建时间计算
                if (userDtoTemp.getModifyPwdTime() != null) {
                    checkTime = userDtoTemp.getModifyPwdTime();
                }
                Long days = Duration.between(checkTime, LocalDateTime.now()).toDays();
                if (days >= Long.parseLong(sysDictUpdatePassWord.getRemark())) {
                    loginObj.put("modifyPwd", 0);
                }
            }

            // 获取当前用户的所有菜单权限(permission)
            String permissions = sysUserService.getPermissionByUser(userDtoTemp.getId());
            loginObj.put("permissions", permissions);

            // 记录登录数据
            this.dealLoginHis(userDtoTemp);

            return Result.success("登录成功(Login Success.)", loginObj);
        }
        else {
            // 密码输入错误上限5次,达到上限次数锁定账号5分钟
            int maxWrongPwdTimes = IntEnum.FIVE.getValue();
            String dictId = "2";
            SysDictDTO dict = sysDictService.findById(dictId);
            String lockMinutes = dict.getRemark().trim();

            // 2021-10-08 增加逻辑：上次输错后10分钟再尝试时，重置错误次数后再累计，避免输错4次后，过了很久输错一次直接锁定的不合理情况
            if (userDtoTemp.getUserLockTime() == null) {
                userDtoTemp.setWrongPwdTimes(0);
            }
            else {
                if (LocalDateTime.now().isAfter(userDtoTemp.getUserLockTime().plusMinutes(Long.parseLong(lockMinutes)))) {
                    userDtoTemp.setWrongPwdTimes(0);
                }
            }

            int wrongTimes = userDtoTemp.getWrongPwdTimes() + IntEnum.ONE.getValue();
            if (wrongTimes < maxWrongPwdTimes) {
                wrapper.lambda()
                        .eq(SysUserDTO::getId, userDtoTemp.getId())
                        .set(SysUserDTO::getWrongPwdTimes, wrongTimes)
                        .set(SysUserDTO::getUserLockTime, LocalDateTime.now());
                sysUserService.update(wrapper);
                throw new ServiceException(
                        StrUtil.format("用户名或密码错误(Password Error) 剩余输入次数：{}次", maxWrongPwdTimes - wrongTimes));
            }
            else {
                wrapper.lambda()
                        .eq(SysUserDTO::getId, userDtoTemp.getId())
                        .set(SysUserDTO::getWrongPwdTimes, wrongTimes)
                        .set(SysUserDTO::getUserLockTime, LocalDateTime.now()
                                .plusMinutes(Long.parseLong(lockMinutes)));
                sysUserService.update(wrapper);
                throw new ServiceException(StrUtil.format("用户名或密码错误(Password Error) 账户锁定{}分钟", lockMinutes));
            }
        }

    }

    /**
     * 记录登录数据
     *
     * @param userDtoTemp 人员信息
     */
    private void dealLoginHis(SysUserDTO userDtoTemp) {
        String ip = getIpAddress(getRequest());
        LocalDateTime now = LocalDateTime.now();
        SysLoginHisVO his = new SysLoginHisVO();
        his.setId(SnowFlakeGenerator.generateIdStr());
        his.setUserId(userDtoTemp.getId());
        his.setLoginTime(now);
        his.setLoginIp(ip);
        his.setCreateBy(userDtoTemp.getId());
        his.setCreateTime(now);
        sysLoginHisService.saveSysLoginHis(his);
    }


    /**
     * 方法功能描述:
     * < 登出系统 >
     *
     * @return Result                        【返回值 注释说明】
     * @author YangFan
     * @date 2021-06-22 16:16
     */
    @PostMapping("/logout")
    public Result logout() {
        try {
            String token = SecurityUtils.getSubject()
                    .getPrincipal()
                    .toString();
            String username = JwtUtil.getClaim(token, Constant.USERNAME);
            // 清除可能存在的Shiro权限信息缓存
            if (RedisUtils.exists(Constant.PREFIX_SHIRO_CACHE + username)) {
                RedisUtils.delete(Constant.PREFIX_SHIRO_CACHE + username);
            }
            // 清除当前账号的RedisToken信息
            if (RedisUtils.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username)) {
                RedisUtils.delete(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username);
            }
            return Result.success("登出成功");
        }
        catch (Exception e) {
            return Result.error("登出失败:" + e.getMessage());
        }
    }

    /**
     * 获取ip地址
     *
     * @param request request
     * @return ip地址
     */
    public String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || UN_KNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UN_KNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UN_KNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || UN_KNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || UN_KNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }


    /**
     * 得到request对象
     */
    private HttpServletRequest getRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }


    /**
     * 加密传输登录参数
     *
     * @param httpServletResponse response
     * @param qo                  qo
     * @return map
     */
    @PostMapping("/login_encrypted")
    public Map<String, Object> loginEncrypted(HttpServletResponse httpServletResponse, @RequestBody SysLoginEncryptedQO qo) {
        SysLoginQO sysLoginQO = RSAUtil.getDecodeObj(qo.getJsonData(), qo.getPublicKey(), SysLoginQO.class);
        if (sysLoginQO != null) {
            return this.login(httpServletResponse, sysLoginQO);
        }
        else {
            return Result.error("参数解密错误");
        }
    }


    @PostMapping("/getPublicKey")
    public Result getPublicKey() {
        try {
            RSA rsa = new RSA();
            String publicKey = rsa.getPublicKeyBase64();
            String privateKey = rsa.getPrivateKeyBase64();
            RedisUtils.setObject(Constant.PUBLIC_KEY_PREFIX + publicKey, privateKey, Constant.PUBLIC_KEY_EXPIRE);
            return Result.success(publicKey);
        }
        catch (Exception e) {
            log.error("获取加密公钥私钥失败：" + e.getMessage());
            return Result.error("密钥获取失败");
        }

    }


}
